/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Unsafe from './Unsafe';
import Long from '../util/long/index'
import Constants from './Constants'
import Util from './Util'

export default class BitOutputStream {
    private static BIT_MASK: Array<number> = [0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047,
    4095, 8191, 16383, 32767, 65535, 131071,
    262143, 524287, 1048575, 2097151, 4194303, 8388607,
    16777215, 33554431, 67108863, 134217727, 268435455, 536870911,
    1073741823, 2147483647]; // up to 31 bits

    private outputBase: any;
    private outputAddress: Long= Long.fromNumber(0);
    private outputLimit: Long= Long.fromNumber(0);
    private container: Long = Long.fromNumber(0);
    private bitCount: number = 0;
    private currentAddress: Long= Long.fromNumber(0);

    constructor(outputBase: any, outputAddress: Long, outputSize: number) {
        Util.checkArgument(outputSize >= Constants.SIZE_OF_LONG, "Output buffer too small");

        this.outputBase = outputBase;
        this.outputAddress = outputAddress;
        this.outputLimit = this.outputAddress.add(outputSize).subtract(Constants.SIZE_OF_LONG);

        this.currentAddress = this.outputAddress;
    }

    public addBits(value: number, bits: number): void
    {
        this.container = this.container.or((Long.fromNumber(value)
            .and(BitOutputStream.BIT_MASK[bits])).shiftLeft(this.bitCount));
        this.bitCount += bits;
    }

    public addBitsFast(value: number, bits: number): void
    {
        this.container = this.container.or(Long.fromNumber(value).shiftLeft(this.bitCount));
        this.bitCount += bits;
    }

    public flush(): void
    {
        let bytes: number = this.bitCount >>> 3;

        Unsafe.putLong(this.outputBase, this.currentAddress.toNumber(), this.container);
        this.currentAddress = this.currentAddress.add(bytes);

        if (this.currentAddress.greaterThan(this.outputLimit)) {
            this.currentAddress = this.outputLimit;
        }

        this.bitCount &= 7;
        this.container = this.container.shiftRightUnsigned(bytes * 8);
    }

    public close(): number
    {
        this.addBitsFast(1, 1); // end mark
        this.flush();

        if (this.currentAddress.greaterThanOrEqual(this.outputLimit)) {
            return 0;
        }

        return ((this.currentAddress.subtract(this.outputAddress)).add(this.bitCount > 0 ? 1 : 0)).toInt();
    }
}
